package processing.mode.java.pdex; import org.eclipse.jdt.core.dom.ASTVisitor; import org.eclipse.jdt.core.dom.CompilationUnit; import org.eclipse.jdt.core.dom.MethodDeclaration; import org.eclipse.jdt.core.dom.NumberLiteral; import org.eclipse.jdt.core.dom.SimpleType; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import processing.mode.java.pdex.TextTransform.Edit; import processing.mode.java.preproc.PdePreprocessor; public class SourceUtils { public static final Pattern IMPORT_REGEX = Pattern.compile("(?:^|;)\\s*(import\\s+(?:(static)\\s+)?((?:\\w+\\s*\\.)*)\\s*(\\S+)\\s*;)", Pattern.MULTILINE | Pattern.DOTALL); public static final Pattern IMPORT_REGEX_NO_KEYWORD = Pattern.compile("^\\s*((?:(static)\\s+)?((?:\\w+\\s*\\.)*)\\s*(\\S+))", Pattern.MULTILINE | Pattern.DOTALL); public static List<ImportStatement> parseProgramImports(CharSequence source) { List<ImportStatement> result = new ArrayList<>(); Matcher matcher = IMPORT_REGEX.matcher(source); while (matcher.find()) { ImportStatement is = ImportStatement.parse(matcher.toMatchResult()); result.add(is); } return result; } public static List<Edit> parseProgramImports(CharSequence source, List<ImportStatement> outImports) { List<Edit> result = new ArrayList<>(); Matcher matcher = IMPORT_REGEX.matcher(source); while (matcher.find()) { ImportStatement is = ImportStatement.parse(matcher.toMatchResult()); outImports.add(is); int idx = matcher.start(1); int len = matcher.end(1) - idx; // Remove the import from the main program // Substitute with white spaces result.add(Edit.move(idx, len, 0)); result.add(Edit.insert(0, "\n")); } return result; } // Positive lookahead and lookbehind are needed to match all type constructors // in code like `int(byte(245))` where first bracket matches as last // group in "^int(" but also as a first group in "(byte(". Lookahead and // lookbehind won't consume the shared character. public static final Pattern TYPE_CONSTRUCTOR_REGEX = Pattern.compile("(?<=^|\\W)(int|char|float|boolean|byte)(?=\\s*\\()", Pattern.MULTILINE); public static List<Edit> replaceTypeConstructors(CharSequence source) { List<Edit> result = new ArrayList<>(); Matcher matcher = TYPE_CONSTRUCTOR_REGEX.matcher(source); while (matcher.find()) { String match = matcher.group(1); int offset = matcher.start(1); int length = match.length(); result.add(Edit.insert(offset, "PApplet.")); String replace = "parse" + Character.toUpperCase(match.charAt(0)) + match.substring(1); result.add(Edit.replace(offset, length, replace)); } return result; } public static final Pattern HEX_LITERAL_REGEX = Pattern.compile("(?<=^|\\W)(#[A-Fa-f0-9]{6})(?=\\W|$)"); public static List<Edit> replaceHexLiterals(CharSequence source) { // Find all #[webcolor] and replace with 0xff[webcolor] // Should be 6 digits only. List<Edit> result = new ArrayList<>(); Matcher matcher = HEX_LITERAL_REGEX.matcher(source); while (matcher.find()) { int offset = matcher.start(1); result.add(Edit.replace(offset, 1, "0xff")); } return result; } public static List<Edit> insertImports(List<ImportStatement> imports) { List<Edit> result = new ArrayList<>(); for (ImportStatement imp : imports) { result.add(Edit.insert(0, imp.getFullSourceLine() + "\n")); } return result; } public static List<Edit> wrapSketch(PdePreprocessor.Mode mode, String className, int sourceLength) { List<Edit> edits = new ArrayList<>(); StringBuilder b = new StringBuilder(); // Header if (mode != PdePreprocessor.Mode.JAVA) { b.append("\npublic class ").append(className).append(" extends PApplet {\n"); if (mode == PdePreprocessor.Mode.STATIC) { b.append("public void setup() {\n"); } } edits.add(Edit.insert(0, b.toString())); // Reset builder b.setLength(0); // Footer if (mode != PdePreprocessor.Mode.JAVA) { if (mode == PdePreprocessor.Mode.STATIC) { // no noLoop() here so it does not tell you // "can't invoke noLoop() on obj" when you type "obj." b.append("\n}"); } b.append("\n}\n"); } edits.add(Edit.insert(sourceLength, b.toString())); return edits; } // Verifies that whole input String is floating point literal. Can't be used for searching. // https://docs.oracle.com/javase/specs/jls/se8/html/jls-3.html#jls-DecimalFloatingPointLiteral public static final Pattern FLOATING_POINT_LITERAL_VERIFIER; static { final String DIGITS = "(?:[0-9]|[0-9][0-9_]*[0-9])"; final String EXPONENT_PART = "(?:[eE][+-]?" + DIGITS + ")"; FLOATING_POINT_LITERAL_VERIFIER = Pattern.compile( "(?:^" + DIGITS + "\\." + DIGITS + "?" + EXPONENT_PART + "?[fFdD]?$)|" + "(?:^\\." + DIGITS + EXPONENT_PART + "?[fFdD]?$)|" + "(?:^" + DIGITS + EXPONENT_PART + "[fFdD]?$)|" + "(?:^" + DIGITS + EXPONENT_PART + "?[fFdD]$)"); } // Mask to quickly resolve whether there are any access modifiers present private static final int ACCESS_MODIFIERS_MASK = Modifier.PUBLIC | Modifier.PRIVATE | Modifier.PROTECTED; public static List<Edit> preprocessAST(CompilationUnit cu) { final List<Edit> edits = new ArrayList<>(); // Walk the tree cu.accept(new ASTVisitor() { @Override public boolean visit(SimpleType node) { // replace "color" with "int" if ("color".equals(node.getName().toString())) { edits.add(Edit.replace(node.getStartPosition(), node.getLength(), "int")); } return super.visit(node); } @Override public boolean visit(NumberLiteral node) { // add 'f' to floats String s = node.getToken().toLowerCase(); if (FLOATING_POINT_LITERAL_VERIFIER.matcher(s).matches() && !s.endsWith("f") && !s.endsWith("d")) { edits.add(Edit.insert(node.getStartPosition() + node.getLength(), "f")); } return super.visit(node); } @Override public boolean visit(MethodDeclaration node) { // add 'public' to methods with default visibility int accessModifiers = node.getModifiers() & ACCESS_MODIFIERS_MASK; if (accessModifiers == 0) { edits.add(Edit.insert(node.getStartPosition(), "public ")); } return super.visit(node); } }); return edits; } public static final Pattern COLOR_TYPE_REGEX = Pattern.compile("(?:^|^\\p{javaJavaIdentifierPart})(color)\\s(?!\\s*\\()", Pattern.MULTILINE | Pattern.UNICODE_CHARACTER_CLASS); public static List<Edit> replaceColorRegex(CharSequence source) { final List<Edit> edits = new ArrayList<>(); Matcher matcher = COLOR_TYPE_REGEX.matcher(source); while (matcher.find()) { int offset = matcher.start(1); edits.add(Edit.replace(offset, 5, "int")); } return edits; } public static final Pattern NUMBER_LITERAL_REGEX = Pattern.compile("[-+]?[0-9]*\\.?[0-9]+(?:[eE][-+]?[0-9]+)?"); public static List<Edit> fixFloatsRegex(CharSequence source) { final List<Edit> edits = new ArrayList<>(); Matcher matcher = NUMBER_LITERAL_REGEX.matcher(source); while (matcher.find()) { int offset = matcher.start(); int end = matcher.end(); String group = matcher.group().toLowerCase(); boolean isFloatingPoint = group.contains(".") || group.contains("e"); boolean hasSuffix = end < source.length() && Character.toLowerCase(source.charAt(end)) != 'f' && Character.toLowerCase(source.charAt(end)) != 'd'; if (isFloatingPoint && !hasSuffix) { edits.add(Edit.insert(offset, "f")); } } return edits; } static public String scrubCommentsAndStrings(String p) { StringBuilder sb = new StringBuilder(p); scrubCommentsAndStrings(sb); return sb.toString(); } static public void scrubCommentsAndStrings(StringBuilder p) { final int length = p.length(); final int OUT = 0; final int IN_BLOCK_COMMENT = 1; final int IN_EOL_COMMENT = 2; final int IN_STRING_LITERAL = 3; final int IN_CHAR_LITERAL = 4; int blockStart = -1; int prevState = OUT; int state = OUT; for (int i = 0; i <= length; i++) { char ch = (i < length) ? p.charAt(i) : 0; char pch = (i == 0) ? 0 : p.charAt(i-1); // Get rid of double backslash immediately, otherwise // the second backslash incorrectly triggers a new escape sequence if (pch == '\\' && ch == '\\') { p.setCharAt(i-1, ' '); p.setCharAt(i, ' '); pch = ' '; ch = ' '; } switch (state) { case OUT: switch (ch) { case '\'': state = IN_CHAR_LITERAL; break; case '"': state = IN_STRING_LITERAL; break; case '*': if (pch == '/') state = IN_BLOCK_COMMENT; break; case '/': if (pch == '/') state = IN_EOL_COMMENT; break; } break; case IN_BLOCK_COMMENT: if (pch == '*' && ch == '/' && (i - blockStart) > 1) { state = OUT; } break; case IN_EOL_COMMENT: if (ch == '\r' || ch == '\n') { state = OUT; } break; case IN_STRING_LITERAL: if ((pch != '\\' && ch == '"') || ch == '\r' || ch == '\n') { state = OUT; } break; case IN_CHAR_LITERAL: if ((pch != '\\' && ch == '\'') || ch == '\r' || ch == '\n') { state = OUT; } break; } // Terminate ongoing block at last char if (i == length) { state = OUT; } // Handle state changes if (state != prevState) { if (state != OUT) { // Entering block blockStart = i + 1; } else { // Exiting block int blockEnd = i; if (prevState == IN_BLOCK_COMMENT && i < length) blockEnd--; // preserve star in '*/' for (int j = blockStart; j < blockEnd; j++) { char c = p.charAt(j); if (c != '\n' && c != '\r') p.setCharAt(j, ' '); } } } prevState = state; } } static public List<JavaProblem> checkForMissingBraces(StringBuilder p, int[] tabStartOffsets) { List<JavaProblem> problems = new ArrayList<>(0); tabLoop: for (int tabIndex = 0; tabIndex < tabStartOffsets.length; tabIndex++) { int tabStartOffset = tabStartOffsets[tabIndex]; int tabEndOffset = (tabIndex < tabStartOffsets.length - 1) ? tabStartOffsets[tabIndex + 1] : p.length(); int depth = 0; int lineNumber = 0; for (int i = tabStartOffset; i < tabEndOffset; i++) { char ch = p.charAt(i); switch (ch) { case '{': depth++; break; case '}': depth--; break; case '\n': lineNumber++; break; } if (depth < 0) { JavaProblem problem = new JavaProblem("Found one too many } characters without { to match it.", JavaProblem.ERROR, tabIndex, lineNumber); problem.setPDEOffsets(i - tabStartOffset, i - tabStartOffset + 1); problems.add(problem); continue tabLoop; } } if (depth > 0) { JavaProblem problem = new JavaProblem("Found one too many { characters without } to match it.", JavaProblem.ERROR, tabIndex, lineNumber - 1); problem.setPDEOffsets(tabEndOffset - tabStartOffset - 2, tabEndOffset - tabStartOffset - 1); problems.add(problem); } } return problems; } }